Hu3sky's blog

CVE-2018-1270 Remote Code Execution with spring-messaging 分析

Word count: 2,378 / Reading time: 10 min
2019/12/20 Share

漏洞详情

Spring Messaging 属于Spring Framework项目,其定义了Enterprise Integration Patterns典型实现的接口及相关的支持(注解,接口的简单默认实现等)

https://pivotal.io/security/cve-2018-1270

影响范围

1
2
3
4
5
6
Spring
versions 5.0.x - 5.0.5
versions 4.3.x - 4.3.16

SpringBoot
2.0.0及以下

Spring Framework允许应用程序通过spring-messaging模块通过简单的内存STOMP代理通过WebSocket端点公开STOMP。恶意用户(或攻击者)可以向代理发送消息,这可能导致远程执行代码攻击。

复现环境
https://repo.spring.io/release/org/springframework/spring/5.0.0.RELEASE/

SPEL

spel是Spring Expression Language 即,spring表达式语言,是一个支持查询和操作运行时对象导航图功能的强大的表达式语言.支持以下功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
文字表达式
布尔和关系运算符
正则表达式
类表达式
访问 properties, arrays, lists, maps
方法调用
关系运算符
参数
调用构造函数
Bean引用
构造Array
内嵌lists
内嵌maps
三元运算符
变量
用户定义的函数
集合投影
集合筛选
模板表达式

一个hello world

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class Spel {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
String var1 = (String)parser.parseExpression("'Hello World'").getValue();
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();
System.out.println(maxValue);
System.out.println(var1);
}
}

image

弹calc
代码

1
String calc = (String)parser.parseExpression("T(Runtime).getRuntime().exec('open /Applications/Calculator.app/')").getValue();

image

这里的T是类型操作符,可以从类路径加载指定类名称(全限定名)所对应的 Class 的实例,格式为:T(全限定类名),效果等同于 ClassLoader#loadClass()

Websocket

由于HTTP具有单向通信的特点,于是造成了Server向Client推送消息变得很难,需要使用轮询的方式,于是有了WebSocket,他支持双向通信,类似于聊天室模式,在这个会话里,Server和Clinet都能发送数据,相互通信,所以WebSocket是一种在一个TCP连接上能够全双工,双向通信的协议

Stomp

STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,一个非常简单和容易实现的协议,提供了可互操作的连接格式,易于开发并应用广泛。这个协议可以有多种载体,可以通过HTTP,也可以通过WebSocket。在Spring-Message中使用的是STOMP Over WebSocket。

STOMP是一种基于帧的协议, STOMP是基于Text的,但也允许传输二进制数据。 它的默认编码是UTF-8,但它的消息体也支持其他编码方式,比如压缩编码。

一个STOMP帧由三部分组成:命令,Header(头信息),Body(消息体)

1
2
3
4
5
COMMAND
header1:value1
header2:value2

Body^@

一个实际帧结构

1
2
3
4
5
SEND
destination:/broker/roomId/1
content-length:57

{“type":"ENTER","content":"o7jD64gNifq-wq-C13Q5CRisJx5E"}

spring 的消息功能是基于消息代理构建的

demo

stomp的架构图
image

图中各个组件介绍:

  • 生产者型客户端(左上组件): 发送SEND命令到某个目的地址(destination)的客户端。
  • 消费者型客户端(左下组件): 订阅某个目的地址(destination), 并接收此目的地址所推送过来的消息的客户端。
  • request channel: 一组用来接收生产者型客户端所推送过来的消息的线程池。
  • response channel: 一组用来推送消息给消费者型客户端的线程池。
  • broker: 消息队列管理者,也可以成为消息代理。它有自己的地址(例如“/topic”),客户端可以向其发送订阅指令,它会记录哪些订阅了这个目的地址(destination)。
  • 应用目的地址(图中的”/app”): 发送到这类目的地址的消息在到达broker之前,会先路由到由应用写的某个方法。相当于对进入broker的消息进行一次拦截,目的是针对消息做一些业务处理。
  • 非应用目的地址(图中的”/topic”,也是消息代理地址): 发送到这类目的地址的消息会直接转到broker。不会被应用拦截。
  • SimpAnnotatonMethod: 发送到应用目的地址的消息在到达broker之前, 先路由到的方法. 这部分代码是由应用控制的。
1
2
3
4
git clone https://github.com/spring-guides/gs-messaging-stomp-websocket
cd gs-messaging-stomp-websocket
//回到spring-boot 2.0.1之前
git checkout 6958af0b02bf05282673826b73cd7a85e84c12d3

几个重要的文件
GreetingController.java(相当于图中的SimpAnnotatonMethod)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package hello;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class GreetingController {


//使用MessageMapping注解来标识所有发送到“/hello”这个destination的消息,都会被路由到这个方法进行处理
@MessageMapping("/hello")
//使用SendTo注解来标识这个方法返回的结果,都会被发送到它指定的destination,“/topic/greetings”.
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(1000); // simulated delay
return new Greeting("Hello, " + message.getName() + "!");
}

}

尤其注意,这个处理器方法有一个返回值,这个返回值并不是返回给客户端的,而是转发给消息代理的,如果客户端想要这个返回值的话,只能从消息代理订阅

WebSocketConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

//使用Configuration注解标识这是一个Springboot的配置类.
@Configuration
//使用此注解来标识使能WebSocket的broker.即使用broker来处理消息
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
//应用程序以 /app 为前缀,而 代理目的地以 /topic 为前缀
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/gs-guide-websocket").withSockJS();
}

}

运行后
image

漏洞复现

static/app.js
添加header,需要指定为selector,在文档里有说明,是一个选择器
https://stomp.github.io/stomp-specification-1.0.html

image

随便发送一条消息即可触发

image
可以抓个包看到websocket发送的STOMP帧
点击connect时
image

image
点击send时
image

漏洞分析

SUBSCRIBE

先在ExecutorSubscribableChannel.classrun函数下断,可以发现先来的第一条命令是CONNECT
image
然后是SUBSCRIBE命令,此时的message如下
image
然后从header取出selector并判断是否为null
image
不为null后,调用expressionParser.parseExpression处理selector,返回给expression变量,
image
然后调用subscriptionRegistry.addSubscription方法添加订阅
传入sessionIdsubsId,destinationexpression,首先从sessionid中获得info,如果没有就注册订阅,存储着session里的东西,返回DefaultSubscriptionRegistry的实例info
image
然后获取value,这里为null

1
DefaultSubscriptionRegistry.SessionSubscriptionInfo value = (DefaultSubscriptionRegistry.SessionSubscriptionInfo)this.sessions.putIfAbsent(sessionId, info);

往下走,添加订阅
image
把destination put 进destinationLookup
image
然后调用Subscription初始化id和selector值,最后add进subs变量
image
image
image

然后在缓存中更新订阅
image

Send

发送的message为
image
前面的调用很多,我们之间从处理的地方开始跟进
image
在这之前的调用栈,通过反射来调到我们请求的方法
image
inovke处理完后,会返回我们的控制器里的值
image

往下执行,跟入handleReturnValue
image
调用getReturnValueHandler来获得handler变量,用于处理return数据,有默认的defaultDestinationPrefix

image
继续跟进
image
提取headers等信息
image
image
然后get Sessionidc01pvxeq
获取destination
image
进入for循环后,首先调用createHeaders,把sessionid传入,主要是设置session和header
image

然后跟到convertAndSend
image
调用doConvert函数得到message,然后调用send,开始发送message
image

image
调用sendInternal
image
message
image
由于timeout为-1,所以调用send传入message参数
image
image

跟入sendInternal
image
image

image
image
调用SimpleBrokerMessageHandlerhandleMessageInternal
获取到信息后调用updateSessionReadTime更新时间
检测是否是perfix开头,这里发往的订阅目的地是/topic/greetings,所以checkDestinationPrefix为true
调用sendMessageToSubscribers发送message到订阅地址
image
跟入findSubscriptions查找订阅,检测destination,没有error则进入findSubscriptionsInternal函数
image

image
这里的destinationCache变量是
image
还记得一开始处理订阅的时候么,我们有一些put操作,相当于存入缓存了,这里就是一个提取的操作
调用DefaultSubscriptionRegistrygetSubscriptions获取订阅信息,
image
从整个订阅里获取全部的信息然后保存到一个迭代器里,并且赋值给info
image

image

最后根据sessionID add result里
image
最后更新缓存
然后跟进filterSubscriptions
首先判断selector是否存在,存在则进入判断
result变量传入后形参是allMatches
提取出expression,判断不为空
image
然后在getValue执行spel表达式
image
调用链很长
image

修复

https://github.com/spring-projects/spring-framework/commit/e0de9126ed8cf25cf141d3e66420da94e350708a#diff-ca84ec52e20ebb2a3732c6c15f37d37aL217
image
image
image

更改为要重用的静态计算上下文,SimpleEvaluationContext 对于权限的限制更为严格,能够进行的操作更少。只支持一些简单的Map结构。

来看看修改为SimpleEvaluationContext如下代码的执行情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;

public class Spel {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Expression calc = parser.parseExpression("T(Runtime).getRuntime().exec('open /Applications/Calculator.app/')");
calc.getValue(context);

}
}

image

不再弹出calc。

Reference

CATALOG
  1. 1. 漏洞详情
  2. 2. 影响范围
  3. 3. SPEL
  4. 4. Websocket
  5. 5. Stomp
    1. 5.1. demo
  6. 6. 漏洞复现
  7. 7. 漏洞分析
    1. 7.1. SUBSCRIBE
    2. 7.2. Send
  8. 8. 修复
  9. 9. Reference